
/*
 * screen.c
 *
 * Implementation of a terminal screen model.
 *
 * The screen model is implemented as an ring buffer with a line count of
 * height + scrollback. The variable line_top stores the index of the 
 * top screen line in the ring buffer:
 *
 *  *---------------------*
 *  |                     |
 *  |                     |
 *  |  scrollback buffer  |
 *  |                     |
 *  |                     |
 *  +---------------------+
 *  |                     |  <- line_top
 *  |   terminal screen   |
 *  |                     |
 *  +---------------------+
 *
 * So it is very easy to shift the screen to scroll it up and put the lines
 * that leave the screen into the scrollback buffer. This operation is simply
 * done by increasing line_top modulo (height + scroll_back).
 *
 *
 * 23.05.2010, Klaus Hennemann
 */

#include <string.h>
#include <stdlib.h>

#include "screen.h"


const struct Betty_CellAttribute BETTY_DEFAULT_CELL_ATTR = 
{
	.fg_color_idx		= BETTY_COLOR_DEFAULT_FG,
	.bg_color_idx		= BETTY_COLOR_DEFAULT_BG,
	.fg_palette		= BETTY_PALETTE_DEFAULT,
	.bg_palette		= BETTY_PALETTE_DEFAULT,
	.underline		= 0,
	.reverse		= 0,
	.blink			= 0,
	.bold			= 0
};


/*--------------------------------------------------------------------------*/
/*	Utility functions						    */
/*--------------------------------------------------------------------------*/


/* clip X to screen [0 .. width - 1] */
static inline int clip_x (const struct Screen *s, int x)
{
	if (x < 0)
		return 0;

	if (x >= s->width)
		return s->width - 1;

	return x;
}


/* clip Y to screen [0 .. height - 1] */
static inline int clip_y (const struct Screen *s, int y)
{
	if (y < 0)
		return 0;

	if (y >= s->height)
		return s->height - 1;

	return y;
}


/* clip pos to screen */
static inline struct Betty_Position clip_pos (const struct Screen *s,
					struct Betty_Position pos)
{
	struct Betty_Position p;

	p.x = clip_x(s, pos.x);
	p.y = clip_y(s, pos.y);

	return p;
}


/* clip rect to screen */
static inline struct Betty_Rect clip_rect (const struct Screen *s,
					struct Betty_Rect rect)
{
	struct Betty_Rect r;

	r.top_left     = clip_pos(s, rect.top_left);
	r.bottom_right = clip_pos(s, rect.bottom_right);
    
	return r;
}


/*
 * Fills the line LINE with CELL_COUNT cells. 
 * On Error <0 is returned, otherwise the cell_count of
 * the line.
 */
static int create_cells (struct Betty_Line *line, int cell_count)
{
	struct Betty_Cell *cell;

	line->cell = (struct Betty_Cell *)
			malloc(cell_count * sizeof(struct Betty_Cell));

	if (!line->cell) {
		line->cell_count = 0;
		return -1;
	}

	line->cell_count = cell_count;

	for (cell = line->cell; cell < line->cell + line->cell_count; cell++) {
		cell->uc = 0x20;
		cell->attr = BETTY_DEFAULT_CELL_ATTR;
	}

	return line->cell_count;
}


/*
 * Resizes the cell-array of line LINE to NEW_SIZE count cells.
 * On error <0 is returned, otherwise the new line size/cell_count.
 */
static int resize_line (struct Betty_Line *line, int new_size)
{
	struct Betty_Cell *cell;

	cell = (struct Betty_Cell *)malloc(new_size * sizeof(*cell));

	if (!cell)
		return -1;

	if (line->cell_count >= new_size) {
		/* shrink line */
		memcpy(cell, line->cell, new_size * sizeof(*cell));	
	} else {
		/* grow line */
		int n;

		memcpy(cell, line->cell, line->cell_count * sizeof(*cell));

		for (n = line->cell_count; n < new_size; n++) {
			cell[n].uc = 0x20;
			cell[n].attr = BETTY_DEFAULT_CELL_ATTR;
		}
	}

	free(line->cell);
	line->cell = cell;
	line->cell_count = new_size;

	return new_size;
}


/*
 * Sets up line Y. That means, if the line has less than
 * screen width cells, the cells array is expanded.
 * On success the address of the line is returned. On
 * error NULL is returned.
 */
static struct Betty_Line * setup_line (struct Screen *s, int y)
{
	struct Betty_Line *line = screen_line(s, y);

	/* create cells if there are none */
	if (!line->cell)
		if (create_cells(line, s->width) < 0)
			return NULL;

	/* if the line is to short, expand it */
	if (line->cell_count < s->width)
		if (resize_line(line, s->width) < 0)
			return NULL;

	return line;
}


/*
 * Sets up the cell at X,Y. Here we perform no clipping, so
 * X,Y must be on screen or scrollback buffer!
 */
static struct Betty_Cell * setup_cell (struct Screen *s, int x, int y)
{
	struct Betty_Line *line;

	if ( !(line = setup_line(s, y)) )
		return NULL;

	return line->cell + x;
}


/*--------------------------------------------------------------------------*/
/*	init, cleanup, resize stuff					    */
/*--------------------------------------------------------------------------*/


int screen_init (struct Screen *s, int width, int height, int scrollback)
{
        memset(s, 0, sizeof(*s));
        s->width	= width;
        s->height       = height;
        s->scrollback   = scrollback;

        /* Allocate lines. */
        s->line_count   = height + scrollback;
        s->line_top     = scrollback;
        s->line         = (struct Betty_Line *)
                        calloc(s->line_count, sizeof(struct Betty_Line));

        if (!s->line)
                return -1;

        /* user interface */
        s->ui           = NULL;
        s->ui_ops       = NULL;

        return 0;
}


void screen_set_ui (struct Screen *s, void *ui,
                        const struct Betty_UiOperations *ops)
{
        s->ui = ui;
        s->ui_ops = ops;
}


void screen_cleanup (struct Screen *s)
{
        struct Betty_Line *line;

        if (!s)
                return;

        for (line = s->line; line < s->line + s->line_count; line++)
                if (line->cell)
                        free(line->cell);

        if (s->line) {
                free(s->line);
                s->line = NULL;
        }
}


/*
 * Fress the cells of line LINE. Additionally the cell pointer
 * and cell count is reset to prevent multifle frees.
 */
static void free_cells (struct Betty_Line *line)
{
	if (line->cell) {
		free(line->cell);
		line->cell = NULL;
		line->cell_count = 0;
	}
}


/*
 * Resizes the line array to new line count (= HEIGHT + SCROLLBACK).
 * On error <0 is returned.
 */
static int resize_line_array (struct Screen *s, int height, int scrollback)
{
	struct Betty_Line *new_line, *l;
	int new_line_count;
	int y, ystart;;

	new_line_count = height + scrollback;

	/* allocate new line array */
	new_line = (struct Betty_Line *)
		calloc(new_line_count, sizeof(struct Betty_Line));

	if (!new_line)
		return -1;

	/* copy old lines to new line array. */
	ystart = (new_line_count < s->line_count) ?
			s->height - new_line_count : -s->scrollback;

	for (l = new_line, y = ystart; y <= s->height - 1; l++, y++)
		*l = *screen_line(s, y);

	/* if new line count is less than old line count we need to 
	   free all superfluous old lines */
	for (y = -s->scrollback; y < ystart; y++)
		free_cells(screen_line(s, y));

	/* switch to new line array */
	free(s->line);
	s->line 	= new_line;
	s->line_count 	= new_line_count;
	s->line_top 	= scrollback;
	s->height 	= height;
	s->scrollback 	= scrollback;

	return 0;
}


/*
 * Resizes the screen model S to the new values WIDTH, HEIGHT
 * and SCROLLBACK.
 * On error <0 is returned.
 */
int screen_resize (struct Screen *s, int width, int height, int scrollback)
{
	int new_line_count = height + scrollback;

	/* If there is no resize, we are done */
	if (s->width == width && s->height == height && 
				s->scrollback == scrollback)
		return 0;

	/* sanity check */
	if (width <= 0 || height <= 0 || scrollback < 0)
		return - 1;

	/* The width of lines is adjusted by the fly, so we can safely use
	   the new value */
	s->width = width;

	/* For changes in height and or scrollback, we need to resize the
	   line-array, or in the best case, only modify the line_top
	   index */
	if (new_line_count == s->line_count) {
		s->line_top = (s->line_top + s->height - height + 
				s->line_count) % s->line_count;
	} else if (resize_line_array(s, height, scrollback) < 0)
		return -1;

	/* maybe the cursor is now out of screen. Adjust it */
	s->cursor = clip_pos(s, s->cursor);

	/* notify the ui */
	if (s->ui_ops) {
		if (s->ui_ops->resizeScreen)
			s->ui_ops->resizeScreen(s->ui, width, height);

		if (s->ui_ops->moveCursor)
			s->ui_ops->moveCursor(s->ui, s->cursor);
	}

	return 0;
}


/*--------------------------------------------------------------------------*/
/*      CURSOR CONTROL                                                      */
/*--------------------------------------------------------------------------*/


/*
 *  Moves the cursor to position X/Y
 */
void screen_cursor_goto (struct Screen *s, int x, int y)
{
	s->cursor.x = clip_x(s, x); 
	s->cursor.y = clip_y(s, y);

        if (s->ui_ops && s->ui_ops->moveCursor)
                s->ui_ops->moveCursor(s->ui, s->cursor);
}


/*--------------------------------------------------------------------------*/
/*      CONTENT FUNCTIONS                                                   */
/*--------------------------------------------------------------------------*/

/*
 * Returns the address of line number Y. Y must be in the interval
 * [-scrollback .. height - 1]
 * where [-scrollback .. -1] is the whole scroll back buffer and
 * [0 .. height - 1] is the whole terminal screen.
 *
 * This function never returns an invalid address. But if Y is not in
 * the correct interval, the address of the wrong line is returned.
 */
struct Betty_Line * screen_line (struct Screen *s, int y)
{
        int idx;
        
        idx = ((int)s->line_top + y) % s->line_count;

        if (idx < 0)
                idx += s->line_count;

        return s->line + idx;
}


/*
 * Erases the cells in rect R. This function performs no clipping
 * and no UI-notification. 
 */
static void erase (struct Screen *s, const struct Betty_Rect *r)
{
	int y;

	for (y = r->top; y <= r->bottom; y++) {
		struct Betty_Line *line = screen_line(s, y);
		struct Betty_Cell *c, *c_end;

		if (!line->cell)
			continue;

		c_end = (r->right < line->cell_count) ?
			line->cell + r->right :
			line->cell + line->cell_count - 1;

		for (c = line->cell + r->left; c <= c_end; c++) {
			c->uc = 0x20;
			c->attr = BETTY_DEFAULT_CELL_ATTR;
		}
	}
}


/*
 * Erases the cells in the rec R
 */
void screen_erase (struct Screen *s, const struct Betty_Rect *r)
{
	struct Betty_Rect cr = clip_rect(s, *r);

	erase(s, &cr);

	if (s->ui_ops && s->ui_ops->drawRect)
		s->ui_ops->drawRect(s->ui, cr);
}


/*
 * Scrolls the whole screen COUNT lines up. Lines that left the
 * screen will be put to the scrollback buffer.
 * This is done very easily by modifying the line_top index.
 */
void screen_shift_up (struct Screen *s, int count)
{
	struct Betty_Rect r;

	if (count <= 0)
		return;

	if (count > s->line_count)
		count = s->line_count;

	s->line_top = (s->line_top + count) % s->line_count;

	r.top	 = s->height - count;
	r.bottom = s->height - 1;
	r.left 	 = 0;
	r.right  = s->width - 1;

	erase(s, &r);

	if (s->ui_ops && s->ui_ops->scrollUp)
		s->ui_ops->scrollUp(s->ui, 0, s->height - 1, count);
}


/*
 * Scrolls the lines in the scrolling range [TOP .. BOTTOM] 
 * COUNT line up.
 *
 * We have two cases:
 *
 * 1. Scroll only a region of the screen but more lines than the
 *    line count of the region. This scroll operation is skipped,
 *    because of this is an simple erase of the screen region.
 *
 * 2. Scroll only a region of the screen but less lines than the
 *    line count of the region. In this case we need to rotate
 *    the lines in the region
 */
void screen_scroll_up (struct Screen *s, int top, int bottom, int count)
{
	struct Betty_Rect r;
	int dy;

	if (count <= 0)
		return;

	top    = clip_y(s, top);
	bottom = clip_y(s, bottom);
	dy     = bottom - top + 1;

	if (count > dy)
		count = dy;

	/* rotate the lines in the interval [TOP .. BOTTOM] */
	if (count < dy) {
        	int y;

		for (y = top; y <= bottom - count; y++) {
			struct Betty_Line l;

			l = *screen_line(s, y);
			*screen_line(s, y) = *screen_line(s, y + count);
			*screen_line(s, y + count) = l;
		}
        }

	r.top	 = bottom - count + 1;
	r.bottom = bottom;
	r.left 	 = 0;
	r.right  = s->width - 1;

	erase(s, &r);

	if (s->ui_ops && s->ui_ops->scrollUp)
		s->ui_ops->scrollUp(s->ui, top, bottom, count);
}


/*
 * Scrolls the lines in scrolling range [TOP .. BOTTOM] 
 * COUNT line down.
 * 
 * If we have to scroll more lines than the scrolling range lines has,
 * the scrolling is skipped, because of that operation is a simple
 * erase of the scrolling range.
 */
void screen_scroll_down (struct Screen *s, int top, int bottom, int count)
{
	struct Betty_Rect r;
	int dy;

	if (count <= 0)
		return;

	top    = clip_y(s, top);
	bottom = clip_y(s, bottom);
	dy     = bottom - top + 1;

	if (count > dy)
		count = dy;

	/* rotate the lines in the interval [TOP .. BOTTOM] */
	if (count < dy) {
		int y;

		for (y = bottom; y >= top + count; y--) {
			struct Betty_Line l;

			l = *screen_line(s, y);
			*screen_line(s, y) = *screen_line(s, y - count);
			*screen_line(s, y - count) = l;
		}
	}

	r.top    = top;
	r.bottom = top + count - 1;
	r.left   = 0;
	r.right  = s->width - 1;

	erase(s, &r);

	/* notify the ui */
        if (s->ui_ops && s->ui_ops->scrollDown)
                s->ui_ops->scrollDown(s->ui, top, bottom, count);
}


/*
 * Scrolls the cells in the rect R COUNT columns right.
 */
void screen_scroll_right (struct Screen *s, const struct Betty_Rect *r,
				int count)
{
	struct Betty_Rect cr, er;
	int dx;

	if (count <= 0)
		return;

	cr = clip_rect(s, *r);
	dx = cr.right - cr.left + 1;

	if (count > dx)
		count = dx;

	if (count < dx) {
		int y;

		for (y = cr.top; y <= cr.bottom; y++) {
			struct Betty_Line *line;
			int x;
			
			if ( !(line = setup_line(s, y)) )
				continue;

			for (x = cr.right; x >= cr.left + count; x--)
				line->cell[x] = line->cell[x - count];
		}
	}

	er = cr;
	er.right = er.left + count - 1;
	erase(s, &er);

	/* notify the ui */
	if (s->ui_ops && s->ui_ops->drawRect)
		s->ui_ops->drawRect(s->ui, cr);
}


/*
 * Scrolls the cells in the rect R COUNT columns left.
 */
void screen_scroll_left (struct Screen *s, const struct Betty_Rect *r,
				int count)
{
	struct Betty_Rect cr, er;
	int dx;

	if (count <= 0)
		return;

	cr = clip_rect(s, *r);
	dx = cr.right - cr.left + 1;

	if (count > dx)
		count = dx;

	if (count < dx) {
		int y;

		for (y = cr.top; y <= cr.bottom; y++) {
			struct Betty_Line *line;
			int x;
			
			if ( !(line = setup_line(s, y)) )
				continue;

			for (x = cr.left; x <= cr.right - count; x++)
				line->cell[x] = line->cell[x + count];
		}
	}

	er = cr;
	er.left = er.right - count + 1;
	erase(s, &er);

	/* notify the ui */
	if (s->ui_ops && s->ui_ops->drawRect)
		s->ui_ops->drawRect(s->ui, cr);
}


/*
 * Fills the cell under the cursor with character UC and
 * cell attribute ATTR.
 */
void screen_fill_active_cell (struct Screen *s, 
				const struct Betty_CellAttribute *attr,
				Betty_Character uc)
{
	struct Betty_Cell *cell;

	if ( !(cell = setup_cell(s, s->cursor.x, s->cursor.y)) )
		return;

	cell->uc   = uc;
	cell->attr = *attr;

	if (s->ui_ops) {
		if (s->ui_ops->drawCell) {
                	s->ui_ops->drawCell(s->ui, s->cursor, cell);
		} else if (s->ui_ops->drawRect) {
			struct Betty_Rect r;

			r.top_left     = s->cursor;
			r.bottom_right = s->cursor;

			s->ui_ops->drawRect(s->ui, r);
		}
	}
}


/*
 * Fills the rect RECT of the screen with the character UC and
 * the attribute ATTR.
 */
void screen_fill (struct Screen *s, const struct Betty_Rect *rect,
		Betty_Character uc, const struct Betty_CellAttribute *attr)
{
	struct Betty_Rect cr = clip_rect(s, *rect);
	int y;

	for (y = cr.top; y <= cr.bottom; y++) {
		struct Betty_Line *line;
		struct Betty_Cell *c;

		if ( !(line = setup_line(s, y)) )
			continue;

		for (c = line->cell + cr.left;
			c <= line->cell + cr.right;
			c++)
		{
			c->uc = uc;
			c->attr = *attr;
		}
	}

	if (s->ui_ops && s->ui_ops->drawRect)
		s->ui_ops->drawRect(s->ui, cr);
}


/*
 * Simply forward the bell request to the user interface
 */
void screen_bell (struct Screen *s)
{
        if (s->ui_ops && s->ui_ops->bell)
                s->ui_ops->bell(s->ui);
}



